Skip to content

feat(webhooks): add verify_and_decode_webhook for compressed payloads#230

Open
nijeesh-stream wants to merge 2 commits intomasterfrom
nijeeshjoshy/cha-3071-compress-webhook-payloads
Open

feat(webhooks): add verify_and_decode_webhook for compressed payloads#230
nijeesh-stream wants to merge 2 commits intomasterfrom
nijeeshjoshy/cha-3071-compress-webhook-payloads

Conversation

@nijeesh-stream
Copy link
Copy Markdown
Contributor

Summary

Linear: CHA-3071 — Stream Chat backend can now compress outbound webhook payloads with gzip and, for SQS / SNS firehose delivery, base64-wrap the compressed bytes so they remain valid UTF-8 over the queue. This SDK now ships helpers so customers can decompress + verify in one call.

New API

Both methods live on the shared StreamChatInterface base, so they are inherited by both the sync StreamChat and the async StreamChatAsync clients (mirroring the existing verify_webhook):

  • client.decompress_webhook_body(body, content_encoding=None, payload_encoding=None) -> bytes — primitive decode with no signature check.
  • client.verify_and_decode_webhook(body, x_signature, content_encoding=None, payload_encoding=None) -> bytes — convenience method: decode + HMAC-SHA256 verify, returns the raw JSON body as bytes (json.loads accepts bytes directly).

payload_encoding="base64" (or "b64") is for the SQS / SNS firehose envelope. content_encoding="gzip" is for HTTP webhooks with Content-Encoding: gzip. None / "" for either is a no-op, so the regular HTTP webhook path is bytewise identical to today.

A new exception WebhookSignatureError(StreamAPIException) is raised on signature mismatch, malformed gzip, or malformed base64. Unsupported encoding values raise ValueError with a message that names gzip (so customers know which compression algorithm to set on the app config).

Backwards compatibility

  • The existing verify_webhook(request_body, x_signature) -> bool signature and behavior are unchanged.
  • No new third-party dependencies — gzip, base64, hmac, hashlib are all stdlib.

Why a separate stream_chat/webhook.py

The decoding logic is plain Python with no client state. Hosting it in a stand-alone module lets the tests exercise the cross-SDK contract (passthrough, gzip, base64, base64 + gzip, signature mismatch variants, unsupported encodings) without instantiating an HTTP client.

Docs

docs/webhooks/webhooks_overview/webhooks_overview.md gets a new Compressed webhook bodies section with the standard "what / why / before you enable" copy plus Django, Flask, and SQS / SNS usage examples.

Test plan

  • make lint — black, flake8, mypy all clean.
  • pytest stream_chat/tests/test_webhook_compression.py -v — 46 passed (covers verify_webhook BC, every decompress_webhook_body rule, every verify_and_decode_webhook happy path + signature-mismatch variant, both client methods, async happy path).
  • Full make test — runs against a live Stream app (requires STREAM_KEY / STREAM_SECRET); will run on CI.

Made with Cursor

… (CHA-3071)

Stream Chat backend can now compress outbound webhook payloads with gzip
and, for SQS / SNS firehose delivery, base64-wrap the compressed bytes so
they remain valid UTF-8 over the queue. Add two new client methods that
let customers decompress + verify in a single call:

- decompress_webhook_body(body, content_encoding=None, payload_encoding=None)
  primitive decode that handles gzip and/or base64
- verify_and_decode_webhook(body, x_signature, content_encoding=None,
  payload_encoding=None) decode + HMAC-SHA256 verify

Both are exposed on the sync StreamChat and async StreamChatAsync clients
through the shared StreamChatInterface base, mirroring the existing
verify_webhook helper. The existing verify_webhook signature and behavior
are unchanged for backward compatibility.

A new WebhookSignatureError (extends StreamAPIException) is raised on
signature mismatch, malformed gzip, or malformed base64. Unsupported
encoding values raise ValueError with a message that points at the
supported algorithm (gzip).

The decoding logic lives in stream_chat/webhook.py so it can be tested
without instantiating an HTTP client. The new tests cover the cross-SDK
contract: passthrough, gzip round-trip, base64 round-trip, base64 + gzip
(SQS / SNS shape), case-insensitive aliases, every unsupported
content_encoding (br / brotli / zstd / deflate / compress / lz4),
unsupported payload_encoding (hex / url / binary), invalid gzip / base64
input, and three signature-mismatch variants (wrong signature, signature
over compressed bytes, signature over wrapped bytes).

Docs: webhooks_overview.md gets a "Compressed webhook bodies" section
with Django, Flask, and SQS / SNS usage examples.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant